同步(Sync)& 非同步(Async) 、回呼(Callback)、Promise + then() + catch()、async + await + try + catch


筆記大綱

  1. 同步(sync)& 非同步(async)
  2. 處理非同步運算時用的回呼(callback)
  3. ES6 開始為了處理回呼地獄(callback hell)而出現的 Promise、then()、catch()
  4. ES7 出現的 Promise 語法糖:async、await、try、catch

同步(sync)& 非同步(async)

JavaScript 是一種單執行序(single-thread)語言,也就是一次只能做一件事情,例如有下列程式碼:

function showFunction(){
    console.log("This is the function.");
    }

//========================================

console.log("start");
showFunction();
console.log("end");

顯示結果如下,可發現程式碼由上到下逐一讀取:

這樣的程式碼稱為「同步(sync)」,看字面意思會以為是一次同時做很多事情,但事實上正好相反,代表的是一次做一件事情。

但若在程式碼中,不希望因為部分內容尚未執行而卡住,要在其他內容執行過程的同時,就可以執行下個階段的內容,就是「非同步(async)」,例如視窗物件的方法「setTimeout()」或「addEventListener()」等。

若使用「setTimeout()」方法,程式碼範例如下:

console.log("start");

setTimeout(() => {
    console.log("This is the code.");
}, 5000);

console.log("end");

實際執行時,會先跑「console.log(“end”);」,中間的「console.log(“This is the code.”);」要等 5 秒才會出現:

這並不代表 JavaScript 在執行程式碼時,可以同時處理「setTimeout()」與「console.log(“end”);」,而是先把「setTimeout()」的部分先丟給「Web API」處理,自己先往下跑,等「setTimeout()」設定的 5 秒鐘到了再顯示裡面的內容。


回呼(callback)

回呼的字面意思就是「等會再叫你」,回呼函式會被當成參數被放在另一個函式中,要等到適合它出場時才會發生作用。

舉例來說,現有一被放在 setTimeout() 中的程式碼如下:

function First(a){
    setTimeout(() => {
    console.log(`This is ${a}.`);
    }, 5000);
}

//========================================

console.log("start");
console.log(First("Number 1"));
console.log("end");

顯示結果如下:

程式碼剛執行時,First() 函式中的內容仍未定,因此顯示「undefined」,要到 5 秒鐘後,First() 裡面的「setTimeout()」函式才會執行。

為了避免出現上述狀況,我們將「setTimeout()」裡面的內容放進一個回呼函式,就可以等到時間 5 秒後才執行「setTimeout()」:

function First(a, callback){       //參數多了回呼函式callback
    setTimeout(() => {
        callback(`The first is ${a}.`);
    }, 5000);
}

//========================================

console.log("start");

First("Number 1", (firstSentence) => {
    console.log(firstSentence);
});

console.log("end");

程式碼執行時,就不會再出現「undefined」:

回呼地獄(callback hell)

如果要有第二個「setTimeout()」,則要另寫一個回呼函式如下:

function First(a, callback){
    setTimeout(() => {
    callback(`The first is ${a}.`);     //第一個回呼函式等5秒
    }, 5000);
}

function Second(b, callback){
    setTimeout(() => {
    callback(`The second is ${b}`);    //第二個回呼函式再多等3秒
    }, 3000)
}

//========================================

console.log("start");

First("Number 1", (firstSentence) => {
    Second("Number 2", (secondSentence) => {
        console.log(secondSentence);
    } )
    console.log(firstSentence);
});

console.log("end");

顯示結果如下,第一個回呼函式要等 5 秒、第二個回呼函式要再等 3 秒才會執行:

如果還要有第三、第四、第五個回呼函式要執行,程式碼將會變成這樣:

function First(a, callback){
    setTimeout(() => {
    callback(`The first is ${a}.`);     //第一個回呼函式等5秒
    }, 5000);
}

function Second(b, callback){
    setTimeout(() => {
    callback(`The second is ${b}.`);    //第二個回呼函式再等3秒
    }, 3000)
}

function Third(c, callback){
    setTimeout(() => {
    callback(`The third is ${c}.`);     //第三個回呼函式再等3秒
    }, 3000)
}

function Fourth(d, callback){
    setTimeout(() => {
    callback(`The fourth is ${d}.`);    //第四個回呼函式再等3秒
    }, 3000)
}

function Fifth(e, callback){
    setTimeout(() => {
    callback(`The fifth is ${e}.`);    //第五個回呼函式再等3秒
    }, 3000)
}

//========================================

console.log("start");

First("Number 1", (firstSentence) => {
    Second("Number 2", (secondSentence) => {
        Third("Number 3", (thirdSentence) => {
            Fourth("Number 4", (fourthSentence) => {
                Fifth("Number 5", (fifthSentence) => {
                    console.log(fifthSentence);
                })
                console.log(fourthSentence);
            })
            console.log(thirdSentence);
        })
        console.log(secondSentence);
    })
    console.log(firstSentence);
});

console.log("end");

顯示結果如下:

雖然一樣可以達到預期的效果,但是程式碼中出現了人見人怕的「回呼地獄(callback hell)」:

//回呼地獄(callback hell)

First("Number 1", (firstSentence) => {
    Second("Number 2", (secondSentence) => {
        Third("Number 3", (thirdSentence) => {
            Fourth("Number 4", (fourthSentence) => {
                Fifth("Number 5", (fifthSentence) => {
                    console.log(fifthSentence);
                })
                console.log(fourthSentence);
            })
            console.log(thirdSentence);
        })
        console.log(secondSentence);
    })
    console.log(firstSentence);
});

Promise、then()、catch()

為了解決回呼地獄的問題,ES6 起版本的 JavaScript 推出了 Promise,用來表示非同步運算最終結果為成功或失敗的物件,大幅提高程式碼的可閱讀性。

Promise 的基本架構如下:

new Promise(function(resolve, reject) => { … });

Promise 的初始狀態是「pending」,而對於用 Promise 創造出來的實例而言,函式中的「resolve」代表成功的狀況,會對應「.then()」方法;「reject」代表失敗的狀況,對應「.catch()」方法。

上述產生回呼地獄的程式碼可以用Promise改寫如下,函式中「resolve」與「reject」兩狀況以三元條件運算子(ternary operator)表示:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

//=======================================================================================================

console.log("start");

First("Number 1")
    .then(() => {
        return Second("Number 2");
    })
    .then(() => {
        return Third("Number 3");
    })
    .then(() => {
        return Fourth("Number 4");
    })
    .then(() => {
        return Fifth("Number 5");
    })
    .catch((error) => {
        return error;
 })

console.log("end");

顯示結果如下,與使用回呼函式相同:

Promise 中的錯誤結果

若要回傳錯誤結果、測試「.catch()」跑到「reject」的狀況,可以在輸入的引數改為一個虛值(falsy value)或空白。

以在 Second() 函式輸入「null」範例如下:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

//=======================================================================================================

console.log("start");

First("Number 1")
    .then(() => {
        return Second(null);   //這裡改成null
    })
    .then(() => {
        return Third("Number 3");
    })
    .then(() => {
        return Fourth("Number 4");
    })
    .then(() => {
        return Fifth("Number 5");
    })
    .catch((error) => {
        return error;
 })

console.log("end");

顯示結果如下:


async、await、try、catch

在 ES7 起版本的 JavaScript,出現了可閱讀性比 Promise 更高的語法糖「async / await」,可以把所有原本寫在「.then()」的內容全部放在「await」中,再把所有的「await」放進一個「async function expression」中。

為了保有確認正確或錯誤的功能,可以把所有的「await」放進「try」中、錯誤的內容放進「catch」:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

//這裡改成async function,命名為tryAsync()
async function tryAsync() {
    try {
        await First("Number 1");
        await Second("Number 2");
        await Third("Number 3");
        await Fourth("Number 4");
        await Fifth("Number 5");
    }
    catch(error) {
        return error;
    };
}

//=======================================================================================================

console.log("start");

tryAsync();

console.log("end");

顯示結果如下:

「async function expression」中的錯誤結果

現將 Second() 中的引數(argument)改為虛值(falsy value)「undefined」如下:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

async function tryAsync() {
    try {
        await First("Number 1");
        await Second(undefined);      //這裡改成undefined,為一個falsy value
        await Third("Number 3");
        await Fourth("Number 4");
        await Fifth("Number 5");
    }
    catch(error) {
        return error;
    };
}

//=======================================================================================================

console.log("start");

tryAsync();

console.log("end");

顯示結果如下,一樣會因為出現錯誤而到「catch()」部分,出現 Second() 中的「reject」區塊:

#javascript #同步/非同步 #回呼 #Promise #Async/Await







你可能感興趣的文章

如何使用 Google Cartographer SLAM 演算法來建地圖

如何使用 Google Cartographer SLAM 演算法來建地圖

Firebase iOS (3) 登入主頁完成篇

Firebase iOS (3) 登入主頁完成篇

Is the End of Data Analysis Near? The Impact of Code Interpreter, the New Feature of GPT-4

Is the End of Data Analysis Near? The Impact of Code Interpreter, the New Feature of GPT-4






留言討論